//+---------------------------------------------------------------------------
//
// Copyright (C) Microsoft Corporation, 2007
//
// File: DataProvider.cs
//
// Contents: Implementation of IDataProvider - direct database access version
//
// Created by: Adam Wardynski (adamwa) Feb 13 2008
//
//----------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Reflection;
using Microsoft.MSAS.ManagedAPI;
using MRS.FrontEnd.BO;

namespace Microsoft.Reputation.Services
{
    /// <summary>
    /// The DatabaseDataProvider class is the IDataProvider layer that communicates 
    /// with the IDatabaseAccessDS layer.  The two main functions are to handle 
    /// Lookups and to handle Overrides.
    /// Basic Functionality
    /// 1) Lookups
    ///    * Parse the DataBlock, that represents the request, to get out all of the 
    ///      ObjectIdentifiers (ObjectType and SHA256) that were sent into the Delivery Service
    ///    * Gather all remaining ObjectIdentifiers (Override Lookup Scenario)
    ///    * Call the database layer with all of the collected ObjectIdentifiers
    ///    * Parse the responses from the database and convert them into DPLookupResponses
    ///      by assembling byte[] gotten from the database by using the requests (DataBlock
    ///      and ObjectIdentifiers)
    ///    Note: The MSASApi is used for parsing the DataBlock as it will apply Bloom logic 
    ///      where it is needed.
    /// 2) Overrides - Override Thread makes a request to this layer on a regular basis to 
    ///      get any new overrides from the database
    ///    * Given a stamp, call the database to get new overrides
    ///    * Package the overrides up for conusumption by the thread object
    /// </summary>
    public class DatabaseDataProvider : IDataProvider
    {
        private static IDatabaseAccessDS s_databaseAccess;
        private static IMsasApiRequestProcessor DataBlockProcessor { get; set; }
        private static bool s_isInitialized = false;
        private static object s_initializationSyncRoot = new object();

        internal static IDatabaseAccessDS DatabaseAccess
        {
            get
            {
                return s_databaseAccess;
            }
            set
            {
                s_databaseAccess = value;
            }
        }

        #region Initialize and Shutdown logic

        public DatabaseDataProvider()
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                lock (s_initializationSyncRoot)
                {
                    if (s_isInitialized)
                    {
                        TraceUtility.TraceWarning(Tracing.Source, "Initialize called for already initialized Database Data Provider");
                    }
                    else
                    {
                        DatabaseAccess = CreateDatabaseAccess();

                        DataBlockProcessor = new MsasApiRequestProcessor();

                        s_isInitialized = true;
                    }
                }
            }
        }

        internal DatabaseDataProvider(IDatabaseAccessDS databaseAccess, IMsasApiRequestProcessor dataBlockProcessor)
        {
            DatabaseAccess = databaseAccess;
            DataBlockProcessor = dataBlockProcessor;
            s_isInitialized = true;
        }

        public void Dispose()
        {
        }

        private static IDatabaseAccessDS CreateDatabaseAccess()
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                Type theDatabaseAccessType = null;
                IDatabaseAccessDS databaseAccess = null;

                try
                {
                    theDatabaseAccessType = Type.GetType(DeliveryServiceConfig.DatabaseAccessType);

                    if (theDatabaseAccessType != null)
                    {
                        ConstructorInfo constructor = theDatabaseAccessType.GetConstructor(Type.EmptyTypes);
                        if (constructor != null)
                        {
                            databaseAccess = constructor.Invoke(new object[0]) as IDatabaseAccessDS;
                        }
                    }
                }
                catch (Exception ex)
                {
                    TraceUtility.TraceCatchException(Tracing.Source, ex);
                    TraceUtility.TraceAssert(Tracing.Source, false, "Failed to initialize specified IDatabaseAcecss");
                    TraceUtility.TraceThrowException(Tracing.Source, ex);
                    throw;
                }

                if (databaseAccess == null)
                {
                    try
                    {
                        theDatabaseAccessType = Type.GetType(DeliveryServiceConfig.DatabaseAccessTypeDefault);
                        ConstructorInfo constructor = theDatabaseAccessType.GetConstructor(Type.EmptyTypes);
                        databaseAccess = (IDatabaseAccessDS)constructor.Invoke(new object[0]);
                    }
                    catch (Exception ex)
                    {
                        TraceUtility.TraceCatchException(Tracing.Source, ex);
                        TraceUtility.TraceAssert(Tracing.Source, false, "Failed to initialize default IDatabaseAcecss");
                        TraceUtility.TraceThrowException(Tracing.Source, ex);
                        throw;
                    }
                }

                return databaseAccess;
            }
        }

        #endregion

        #region Dealing with Lookup

        internal class DbLookupState
        {
            public IEnumerable<DPLookupRequest> Requests;
            public long DbLatencyMilliSec;
            public long DbLatencyTicks;

            internal DbLookupState(
                IEnumerable<DPLookupRequest> requests)
            {
                this.Requests = requests;
                this.DbLatencyMilliSec = 0;
                this.DbLatencyTicks = 0;
            }
        }

        internal class DbLookupAsyncResult
            : AsyncResultOwnState<DPLookupResponse[], DbLookupState>
        {
            internal DbLookupAsyncResult(DbLookupState ownState, AsyncCallback hostCallback, object hostState)
                : base(ownState, hostCallback, hostState)
            {
            }
        }

        internal List<ObjectIdentifier> GetObjectIdentifiersFromLookupRequests(IEnumerable<DPLookupRequest> lookupRequests)
        {
            List<ObjectIdentifier> rows = new List<ObjectIdentifier>();
            foreach (DPLookupRequest request in lookupRequests)
            {
                // Currently the datablock is only used by URL lookup scenarios
                // If files are to use it further work will be needed.
                if (request is DPLookupUrlDataBlock)
                {
                    byte[] datablock = ((DPLookupUrlDataBlock)request).DataBlock;
                    MsasDataBlockProcessor processor = new MsasDataBlockProcessor(DataBlockProcessor, datablock);
                    DatabaseRequestProcessorContext context = processor.ProcessRequestForDatabase();

                    rows.AddRange(context.ObjectIdentifiers);
                }
                else
                {
                    DPLookupUrlOverrides overridesRequest = (DPLookupUrlOverrides) request;
                    rows.AddRange(overridesRequest.Overrides);
                }
            }
            return rows;
        }

        internal static DPLookupResponse[] BuildLookupResponsesFromUnpackedObjects(IEnumerable<DPLookupRequest> lookupRequests, List<UnpackedObject> unpackedObjects)
        {
            DPLookupResponse[] responses = new DPLookupResponse[lookupRequests.Count()];

            for (int i = 0; i < lookupRequests.Count(); i++)
            {
                DPLookupRequest request = lookupRequests.ElementAt(i);

                List<UnpackedObject> matches = null;
                DPLookupUrlDataBlock urlDataBlock = request as DPLookupUrlDataBlock;

                // Apply the Bloom logic here and hang onto the results so that we can send back
                // only useful matches and no matches (false hits) to the client
                if (urlDataBlock != null)
                {
                    byte[] datablock = urlDataBlock.DataBlock;
                    MsasDataBlockProcessor processor = new MsasDataBlockProcessor(DataBlockProcessor, datablock);
                    ResponseProcessorContext context = processor.ProcessRequestForResponse(request.ObjectType, unpackedObjects);

                    matches = context.MatchesOut;
                }
                else
                {
                    // Overrides Case
                    DPLookupUrlOverrides overridesRequest = request as DPLookupUrlOverrides;
                    matches = new List<UnpackedObject>(overridesRequest.Overrides.Count);

                    foreach (ObjectIdentifier objectId in overridesRequest.Overrides)
                    {
                        UnpackedObject matchedObject = unpackedObjects.SingleOrDefault(u => u.Sha256.SequenceEqual(objectId.Sha256));
                        if (matchedObject != null)
                        {
                            matches.Add(matchedObject);
                        }
                        else if (request.ObjectType == ObjectType.URL)
                        {
                            // Create a PlaceHolder for URL objects
                            matches.Add(MsasDataBlockProcessor.CreatePlaceholderObject(objectId));
                        }
                    }
                }
                responses[i] = new DPLookupResponse
                {
                    Matches = matches
                };
            }

            return responses;
        }

        private static void GetLookupResult(IAsyncResult ar)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                DbLookupAsyncResult asyncResult = (DbLookupAsyncResult)ar.AsyncState;
                IEnumerable<DPLookupRequest> requests = asyncResult.OwnState.Requests;
                DPLookupResponse[] results = null;
                Exception exception = null;

                try
                {
                    List<UnpackedObject> unpackedObjects = DatabaseAccess.EndDBLookupQuery(ar);
                    TraceUtility.TraceAssert(Tracing.Source, unpackedObjects != null, "UnpackedObject list expected not to be null");
                    results = BuildLookupResponsesFromUnpackedObjects(requests, unpackedObjects);
                }
                catch (Exception ex)
                {
                    TraceUtility.TraceCatchException(Tracing.Source, ex);
                    exception = ex;
                }
                finally
                {
                    if (asyncResult != null)
                    {
                        if (exception == null)
                        {
                            asyncResult.SetComplete(results, ar.CompletedSynchronously);
                        }
                        else
                        {
                            asyncResult.SetComplete(exception, ar.CompletedSynchronously);
                        }
                    }
                }

            }
        }

        public IAsyncResult BeginLookupObjects(
            IEnumerable<DPLookupRequest> requests,
            AsyncCallback callback,
            object state)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                List<ObjectIdentifier> rows = GetObjectIdentifiersFromLookupRequests(requests);

                DbLookupState dbLookupState = new DbLookupState(requests);

                DbLookupAsyncResult asyncResult = new DbLookupAsyncResult(
                    dbLookupState,
                    callback,
                    state);

                DatabaseAccess.BeginDBLookupQuery(rows, GetLookupResult, asyncResult);

                return asyncResult;
            }
        }

        public DPLookupResponse[] EndLookupObjects(
            out uint operationStatus,
            out long dataProviderLatency,
            IAsyncResult asyncResult)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                if (asyncResult == null)
                {
                    throw new ArgumentNullException("asyncResult");
                }

                DbLookupAsyncResult ar = asyncResult as DbLookupAsyncResult;

                DPLookupResponse[] result = ar.EndInvoke();

                operationStatus = StatusCodes.OK;
                dataProviderLatency = ar.OwnState.DbLatencyMilliSec;

                return result;
            }
        }
        #endregion

        #region Dealing with Get Overrides

        public DPOverrideResponse GetOverrides(long latestOverrideStamp)
        {
            using (TraceUtility.TraceMethod(Tracing.Source, "latestOverrideStamp:{0}", latestOverrideStamp))
            {
                List<Override> results = DatabaseAccess.GetOverrides(latestOverrideStamp);

                return new DPOverrideResponse
                    {
                        OperationStatus = StatusCodes.OK,
                        Overrides = results
                    };
            }
        }

        #endregion
    }
}
